package client;




import static java.lang.System.err;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.openssl.PasswordFinder;
import org.bouncycastle.util.encoders.Base64;

import message.Request;
import message.Response;
import message.request.BuyRequest;
import message.request.CreditsRequest;
import message.request.CryptLoginRequest;
import message.request.DownloadFileRequest;
import message.request.DownloadTicketRequest;
import message.request.ListRequest;
import message.request.LoginRequest;
import message.request.LogoutRequest;
import message.request.PCRequest;
import message.request.UploadRequest;
import message.response.BuyResponse;
import message.response.CreditsResponse;
import message.response.DownloadFileResponse;
import message.response.DownloadTicketResponse;
import message.response.ListResponse;
import message.response.LoginResponse;
import message.response.MessageResponse;
import message.response.OKLoginResponse;
import message.response.UserInfoResponse;
import message.response.VersionResponse;
import model.DownloadTicket;
import proxy.IProxy;
import proxy.IRemoteObject;
import sun.security.util.Password;
import util.Config;

public class Proxy implements IProxy, IRemoteObject {

	private String proxyHost;
	private int proxyTcpPort;
	private String mcProxyHost;
	private int mcRMIPort;
	private String mcBindingName;
	private String mcKeysDir;

	private Socket socket;
	//private ObjectOutputStream out;
	//private ObjectInputStream in;
	
	private ObjectInputStream in;
    private ObjectOutputStream out;
    private String mychallenge;
    private PrivateKey privateKey = null;
    private static Cipher rsaencrypt, rsadecrypt;
    private static Cipher aesencrypt, aesdecrypt;
    private boolean cipher;
    private String proxypublickey;
    private String clientkeysdir;
	
	private boolean loggedIn;
	
	private Config messages;
	private IRemoteObject remoteObject;
	private Registry registry;


	public Proxy(String proxyHost, int proxyTcpPort) {
		
		this.proxyHost = proxyHost;
		this.proxyTcpPort = proxyTcpPort;
		messages = new Config("messages");
		readMCconfig();
		rmiConnect();
		
		loggedIn = false;
		cipher = false;
		
		try
	    {
	      rsaencrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
	      rsadecrypt = Cipher.getInstance("RSA/NONE/OAEPWithSHA256AndMGF1Padding");
	      aesencrypt = Cipher.getInstance("AES/CTR/NoPadding");
	      aesdecrypt = Cipher.getInstance("AES/CTR/NoPadding");
	    }
	    catch(NoSuchAlgorithmException e)
	    {
	      System.out.println("Unable to initialize cipher: " + e.getMessage());
	    }
	    catch(NoSuchPaddingException e)
	    {
	      System.out.println("Unable to initialize cipher: " + e.getMessage());
	    }
	}
	
	private void readMCconfig() {
		Config config = new Config("mc");
		
		mcBindingName = config.getString("binding.name");
		mcProxyHost = config.getString("proxy.host");
		mcRMIPort = config.getInt("proxy.rmi.port");
		mcKeysDir = config.getString("keys.dir");
		
		Config configc = new Config("client");
		
		proxypublickey = configc.getString("proxy.key");
		clientkeysdir = configc.getString("keys.dir");
		
		
	}
	
	
	private void rmiConnect() {
		try {
			this.registry = LocateRegistry.getRegistry(this.mcProxyHost, this.mcRMIPort);
			this.remoteObject = (IRemoteObject) registry.lookup(this.mcBindingName);
			
		} catch (RemoteException e) {
			//System.out.println(messages.getString("error.noRMIConnection"));
		} catch (NotBoundException e) {
			//System.out.println(messages.getString("error.noRMIConnection"));
		}
	}
	
	private void rmiReconnect() throws RemoteException, NotBoundException {
		this.registry = LocateRegistry.getRegistry(this.mcProxyHost, this.mcRMIPort);
		this.remoteObject = (IRemoteObject) registry.lookup(this.mcBindingName);
	}

	private Response sendRequest(Request request) {
		Response response = null;
		boolean isLoginRequest = (request instanceof CryptLoginRequest);
		
		if(!loggedIn && !isLoginRequest)
			return new MessageResponse(messages.getString("error.notAuthenticated"));
		else if(loggedIn && isLoginRequest) {
			System.out.println(messages.getString("error.alreadyLoggedIn"));
			return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);
		}
			
		try {
			if (socket == null || socket.isClosed())
				connect();
			
			//out.writeObject(request);
			//response = (Response) in.readObject();
			if(cipher==true){
			
				byte[] buf = encryptaes(request);
			      out.writeObject(buf);
				
			    byte[] bytebuffer = null;
					bytebuffer = (byte[]) in.readObject();	
					Response r=null;
					
					r = (Response) decryptaes(bytebuffer);
					if(r instanceof BuyResponse){ response = (BuyResponse) r; }
					//if(r instanceof CorruptedMessagResponse){ response = (CorruptedMessageResponse) r; }
					if(r instanceof CreditsResponse){ response = (CreditsResponse) r; }
					if(r instanceof DownloadFileResponse){ response = (DownloadFileResponse) r; }
					if(r instanceof DownloadTicketResponse){ response = (DownloadTicketResponse) r; }
					//if(r instanceof FileServerInfoResponse){ response = (FileServerInfoResponse) r; }
					//if(r instanceof InfoResponse){ response = (InfoResponse) r; }
					if(r instanceof ListResponse){ response = (ListResponse) r; }
					if(r instanceof LoginResponse){ response = (LoginResponse) r; }
					if(r instanceof MessageResponse){ response = (MessageResponse) r; }
					if(r instanceof OKLoginResponse){ response = (OKLoginResponse) r; }
					if(r instanceof UserInfoResponse){ response = (UserInfoResponse) r; }
					if(r instanceof VersionResponse){ response = (VersionResponse) r; }
					
					return response;
					}
			      
				
			
			
			else{
			
			out.writeObject(request);
			response = (Response) in.readObject();
			}
			
			
			
			
			
		} catch (Exception e) {
			//e.printStackTrace();
			//cleanup();
			if(isLoginRequest) {
				System.out.println(messages.getString("error.noConnection"));
				return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);
			} else {
				return new MessageResponse(messages.getString("error.noConnection"));
			}
		}

		return response;
	}
	
	

	public void connect() throws IOException {
		try {

			socket = new Socket(this.proxyHost, this.proxyTcpPort);

			//out = new ObjectOutputStream(this.socket.getOutputStream());
			//in = new ObjectInputStream(this.socket.getInputStream());
			
			
			
			out = new ObjectOutputStream(this.socket.getOutputStream());
			in = new ObjectInputStream(this.socket.getInputStream());

		} catch (UnknownHostException e) {
			System.out.println(new MessageResponse(messages.getString("error.unexpected")));
		}
	}

	public void disconnect() throws IOException {
		if (socket != null && !socket.isClosed()) {
			
			socket.shutdownOutput();
			socket.shutdownInput();
			socket.close();
		}
	}

	public void cleanup() {
		
		loggedIn = false;
		
		try {

		if (socket != null && !socket.isClosed())
				socket.close();

			socket = null;
			in = null;
			out = null;

		} catch (IOException e) {
			System.out.println(new MessageResponse(messages.getString("error.unexpected")));
		}
	}

	@Override
	public LoginResponse login(CryptLoginRequest request) throws IOException {
		
	 
		if(loggedIn==true){
	    System.out.println(messages.getString("error.alreadyLoggedIn"));
		return new LoginResponse(LoginResponse.Type.WRONG_CREDENTIALS);
		}
		
	    final String password=request.getPassword();
		
		File pemfile = new File(clientkeysdir, request.getUsername() + ".pem");
	      if (!pemfile.isFile())
	      {
	        System.out.println("Error: Private keyfile '" + pemfile + "' doesn't exist.");
	       
	      }
	      if (!pemfile.canRead())
	      {
	       System.out.println("Error: Private keyfile '" + pemfile + "' is not readable.");
	        
	      }

	      try
	      {
	    	 
	        PEMReader inp = new PEMReader(new FileReader(pemfile), new PasswordFinder(){

	        	@Override
	        	public char[] getPassword() {
	        			
	        		return password.toCharArray();
					
	        	}
	        });
	        
	       
	        
	        KeyPair keyPair = (KeyPair) inp.readObject();
	        privateKey = keyPair.getPrivate();
	        
	        rsadecrypt.init(Cipher.DECRYPT_MODE, privateKey);
	        
	        PEMReader in = new PEMReader(new FileReader(proxypublickey));
	        PublicKey publicKey = (PublicKey) in.readObject();
	        rsaencrypt.init(Cipher.ENCRYPT_MODE, publicKey);
	        
	        if (socket == null || socket.isClosed())
				{connect();} 
	      }
	      catch(FileNotFoundException e)
	      {
	        System.out.println("Error while reading private key of " + request.getUsername() + ". Unable to read keyfile.");
	       
	      }
	      catch(IOException e)
	      {
	        System.out.println("Error while reading private key of " + request.getUsername() + ". Maybe wrong pass phrase.");
	        //e.printStackTrace();
	      }
	      catch(InvalidKeyException e)
	      {
	        System.out.println("Error: invalid key file: " + e.getMessage());
	        
	      }

	      /* generates a 32 byte secure random number */
		
	      SecureRandom secureRandom = new SecureRandom();
	      byte[] tmp = new byte[32];
	      secureRandom.nextBytes(tmp);
	      mychallenge = new String(Base64.encode(tmp));

	    
	    
	     
	     OKLoginResponse response=null;
	     
	     CryptLoginRequest cryptrequest = new CryptLoginRequest(request.getUsername(), mychallenge, null);
	     
	     
	     byte[] buf = encryptrsa(cryptrequest);
	     out.writeObject(buf);
	     
	     
	     byte[] bytebuffer = null;
			
		try {
			bytebuffer = (byte[]) in.readObject();
		} catch (ClassNotFoundException e2) {
			System.out.println("fehler beim lesen");
		}	
		Response ok = null;
		try {
			ok = (Response) decryptrsa(bytebuffer);
		} catch (ClassNotFoundException e1) {
			System.out.println("fehler beim decrypten"); 
		}
			if(ok instanceof OKLoginResponse){
				
				response = (OKLoginResponse) ok;
				
				}
	     
	     
	     
	      String proxychallenge = response.getProxychallange();
	 
	      
	      try
	      {

	          SecretKeySpec seckey = new SecretKeySpec(response.getSecretkey(), "AES");
	          IvParameterSpec iv = new IvParameterSpec(response.getivparameter());
	        aesencrypt.init(Cipher.ENCRYPT_MODE, seckey, iv);
	        aesdecrypt.init(Cipher.DECRYPT_MODE, seckey, iv);
	        
	      }
	      catch(InvalidKeyException e)
	      {
	        err.println("Error: invalid AES key: " + e.getMessage());
	        
	      }
	      catch(InvalidAlgorithmParameterException e)
	      {
	        err.println("Error: invalid AES parameter: " + e.getMessage());
	        
	      }
	      
	      PCRequest pcrequest = new PCRequest(proxychallenge);
	      byte[] bufer = encryptaes(pcrequest);
	      out.writeObject(bufer);
	     
	      
	      loggedIn=true;
	      LoginResponse loginresponse = new LoginResponse(LoginResponse.Type.SUCCESS);
	     
	      
	      
	      cipher=true;
	      
	      return(loginresponse);
	      
		//return null;
		
	//authenticating end
		
	//	LoginResponse response = (LoginResponse) sendRequest(request);
		
	//	loggedIn = (response.getType() == LoginResponse.Type.SUCCESS);
		
	//	return response;
	
	    
	}
	
	
	
	@Override
	public Response credits() throws IOException {
		
		return sendRequest(new CreditsRequest());
		
	}

	@Override
	public Response buy(BuyRequest credits) throws IOException {
		return sendRequest(credits);
	}

	@Override
	public Response list() throws IOException {
		return sendRequest(new ListRequest());
	}

	@Override
	public Response download(DownloadTicketRequest request) throws IOException {
		
		Response response = sendRequest(request);
		DownloadTicketResponse ticketResponse = null;
		
		if(response instanceof MessageResponse)
			return response;

		ticketResponse = (DownloadTicketResponse) response;
		
		DownloadTicket ticket = ticketResponse.getTicket();
		DownloadFileRequest fileRequest = new DownloadFileRequest(ticket);
		
		Socket fsSocket = null;
		ObjectOutputStream fsOut = null;
		ObjectInputStream fsIn = null;
		Response fsResponse = null;
		
		try {
			fsSocket = new Socket(ticket.getAddress(), ticket.getPort());
			fsOut = new ObjectOutputStream(fsSocket.getOutputStream());
			fsIn = new ObjectInputStream(fsSocket.getInputStream());
			
			fsOut.writeObject(fileRequest);
			fsResponse = (Response) fsIn.readObject();
			
		} catch (ClassNotFoundException e) {
			return new MessageResponse(messages.getString("error.unexpected"));
		} catch (IOException e) {
			//force finally block to trigger
			throw new IOException(e);
		} finally {
			fsSocket.shutdownOutput();
			fsSocket.shutdownInput();
			fsSocket.close();
			
			fsSocket = null;
			fsOut = null;
			fsIn = null;
		}
		
		return fsResponse;
	}

	@Override
	public MessageResponse upload(UploadRequest request) throws IOException {
		return (MessageResponse) sendRequest(request);
	}

	@Override
	public MessageResponse logout() throws IOException {

		if(!loggedIn)
			return new MessageResponse(messages.getString("error.notAuthenticated"));
		else if (socket == null || socket.isClosed())
			//return new MessageResponse(messages.getString("error.noConnection"));
			return new MessageResponse("oops3");
		
		MessageResponse response = (MessageResponse) sendRequest(new LogoutRequest());

		loggedIn = false;
		disconnect();

		return response;
	}

	@Override
	public int readQuorum() {
		try {
			return this.remoteObject.readQuorum();
		} catch (Exception e) {
			try {
				rmiReconnect();
				return readQuorum();
			} catch (Exception ex) {
				return -1;
			}
		}
	}

	@Override
	public int writeQuorum() throws RemoteException {
		try {
			return this.remoteObject.writeQuorum();
		} catch (Exception e) {
			try {
				rmiReconnect();
				return writeQuorum();
			} catch (Exception ex) {
				return -1;
			}
		}
	}

	@Override
	public MessageResponse topThreeDownloads() throws RemoteException {
		try {
			return this.remoteObject.topThreeDownloads();
		} catch (Exception e) {
			try {
				rmiReconnect();
				return this.remoteObject.topThreeDownloads();
			} catch (Exception ex) {
				return new MessageResponse(messages.getString("error.noRMIConnection"));
			}
		}
	}
	
	@Override
	public MessageResponse getProxyPublicKey() throws RemoteException {
		try {
			return this.remoteObject.getProxyPublicKey();
		} catch (Exception e) {
			try {
				rmiReconnect();
				return this.remoteObject.getProxyPublicKey();
			} catch (Exception ex) {
				return new MessageResponse(messages.getString("error.noRMIConnection"));
			}
		}
	}
	
	@Override
	public MessageResponse setUserPublicKey(String username, String fileContent) throws RemoteException {
		try {
			return this.remoteObject.setUserPublicKey(username, fileContent);
		} catch (Exception e) {
			try {
				rmiReconnect();
				return this.remoteObject.setUserPublicKey(username, fileContent);
			} catch (Exception ex) {
				return new MessageResponse(messages.getString("error.noRMIConnection"));
			}
		}
	}

	@Override
	public MessageResponse subscribe(String filename, int numberOfDownloads, IRMICallback clientCallback) throws RemoteException {
		if (!loggedIn) {
			return new MessageResponse(messages.getString("error.notAuthenticated"));
		}
		
		try {
			return this.remoteObject.subscribe(filename, numberOfDownloads, clientCallback);	
		} catch (Exception e) {
			try {
				rmiReconnect();
				return this.remoteObject.subscribe(filename, numberOfDownloads, clientCallback);	
			} catch (Exception ex) {
				return new MessageResponse(messages.getString("error.noRMIConnection"));
			}
		}

	}

	@Override
	public MessageResponse rmitest() throws RemoteException {
		try {
			return this.remoteObject.rmitest();
		} catch (Exception e) {
			//return new MessageResponse(messages.getString("error.noConnection"));
			return new MessageResponse("oops");
		}
	}
	
	public static byte[] serialize (Object obj) throws IOException {
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(b);
        o.writeObject(obj);
        return b.toByteArray();
    }

    public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream b = new ByteArrayInputStream(bytes);
        ObjectInputStream o = new ObjectInputStream(b);
        return o.readObject();
    }

    public static byte[] encryptrsa(Object obj) throws IOException{
		
		 byte[] cryptrequestbyte = serialize(obj);
	     byte[] cryptrequestbyteencoded = null;
	     try {
			cryptrequestbyteencoded = rsaencrypt.doFinal(cryptrequestbyte);
		} catch (IllegalBlockSizeException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		} catch (BadPaddingException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		}
	     
	     byte[] cryptrequestbyteencoded64 = Base64.encode(cryptrequestbyteencoded);
		
		
		return cryptrequestbyteencoded64;
		
	}
	
    public static byte[] encryptaes(Object obj) throws IOException{
		
		 byte[] cryptrequestbyte = serialize(obj);
	     byte[] cryptrequestbyteencoded = null;
	     try {
			cryptrequestbyteencoded = aesencrypt.doFinal(cryptrequestbyte);
		} catch (IllegalBlockSizeException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		} catch (BadPaddingException e1) {
			System.out.println("rsa encoding fehlgeschlagen");
		}
	     
	     byte[] cryptrequestbyteencoded64 = Base64.encode(cryptrequestbyteencoded);
		
		
		return cryptrequestbyteencoded64;
		
	}

	public static Object decryptrsa(byte[] b) throws IOException, ClassNotFoundException{
		
		byte[] cryptrequestbyteencoded = Base64.decode(b);
		byte[] cryptrequestbyte = null;
		try {
			cryptrequestbyte = rsadecrypt.doFinal(cryptrequestbyteencoded);
		} catch (IllegalBlockSizeException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		} catch (BadPaddingException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		}
		Object obj = deserialize(cryptrequestbyte);
		
		
		return obj;
		
	}

public static Object decryptaes(byte[] b) throws IOException, ClassNotFoundException{
		
		byte[] cryptrequestbyteencoded = Base64.decode(b);
		byte[] cryptrequestbyte = null;
		try {
			cryptrequestbyte = aesdecrypt.doFinal(cryptrequestbyteencoded);
		} catch (IllegalBlockSizeException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		} catch (BadPaddingException e) {
			System.out.println("rsa decoding fehlgeschlagen");
		}
		Object obj = deserialize(cryptrequestbyte);
		
		
		return obj;
		
	}






}
